0%

与 ImageView 有关的知识点

ImageView

ImageView 的核心功能是显示 Drawable,因此其核心方法是

1
public void setImageDrawable(@Nullable Drawable drawable)

传入新的 Drawable 对象后,会配置其各种属性,包括 level,state,染色,Bounds等。

onDraw 方法就是对 Drawable 对象的绘制,但有两点要注意

  • Matrix 通过左乘对 canvas 产生影响 ,可以用于图片处理
  • mCropToPadding 通过 canvas 的 clipRect 方法将对显示区域做截取, 并将 padding 纳入 Drawable 的Bound计算
1
2
3
4
5
6
7
8
9
10
canvas.save();
if (mCropToPadding) {
canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
scrollX + mRight - mLeft - mPaddingRight,
scrollY + mBottom - mTop - mPaddingBottom);
}
canvas.translate(mPaddingLeft, mPaddingTop);
canvas.concat(mDrawMatrix);
mDrawable.draw(canvas);
canvas.restoreToCount(saveCount);

如果设置了 mCropToPadding 为 true,则滚动ImageView

ImageButton与 FloatingActionButton

子类 ImageButton 虽然名为 “Button”,却不是 TextView,而是 ImageView,只不过它用了与 Button 相同的背景

1
<item name="background">@drawable/btn_default</item>

子类 FloatingActionButton 的特别之处在于它强制定义了背景 Drawable,其默认配置如下

1
2
3
4
5
6
7
8
9
<style name="Widget.Design.FloatingActionButton" parent="android:Widget">
<item name="android:background">@drawable/design_fab_background</item> //白色的圆形 shapedrawable
<item name="backgroundTint">?attr/colorAccent</item> // 背景的渲染色是 colorAccent
<item name="fabSize">normal</item>
<item name="elevation">@dimen/design_fab_elevation</item> // 6dp
<item name="pressedTranslationZ">@dimen/design_fab_translation_z_pressed</item> // 6dp
<item name="rippleColor">?attr/colorControlHighlight</item>
<item name="borderWidth">@dimen/design_fab_border_width</item> // 0.5dp
</style>

然而实际的背景并不是简单地 ShapeDrawable,还要考虑描边和ripple的效果,其实现在不同的版本各不相同。

绘制形状的改造

圆形控件:CircleImageView库实现了圆形图片,实际是重新实现了 ImageView 的绘制方法,它的绘制原理是从 Drawable 中提取出位图 Bitmap 对象,而后使用其作为 BitmapShader 的像素源,绘制圆形图片。

它的具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void onDraw(Canvas canvas) {

//1.获取 Drawable
Drawable drawable = getDrawable();

//2.提取 Bitmap
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), ARGB_8888);
Canvas c = new Canvas(bitmap);
drawable.setBounds(0, 0, c.getWidth(), c.getHeight());
drawable.draw(c);
//3.生成 BitmapShader 以配置 Paint
BitmapShader bitmapShader = new BitmapShader(bitmap, CLAMP, CLAMP);
Paint bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bitmapPaint.setShader(bitmapShader);

//4.计算尺寸和位置,完成最后绘制
int width = getWidth() - getPaddingLeft() - getPaddingRight();
int height = getHeight() - getPaddingTop() - getPaddingBottom();
int radius = Math.min(width / 2, height / 2);
int centerX = getPaddingLeft() + width / 2;
int centerY = getPaddingTop() + height / 2;
canvas.drawCircle(centerX, centerY, radius, bitmapPaint);
}

实际上这也可以通过 canvas 的 clipPath 方法来实现,但截取操作在底层开销比较大,宜使用 Shader 方法。

v4 包中的 CircleImageView 专门用作下拉刷新控件,它的背景 Drawable 设置为圆形的 ShapeDrawable,内容 Drawable 设置为带动画效果的箭头 Drawable。

RoundedImageView库实现圆角图片,其实现原理与上面的 CircleImageView 类似,以(0,0,200,200)为区域,半径为 20 的圆角矩形为例,最终实现方法是

1
2
3
4
5
6
7
@Override
public void draw(@NonNull Canvas canvas) {
BitmapShader bitmapShader = new BitmapShader(mBitmap, mTileModeX, mTileModeY);
//...
canvas.drawRoundRect(mDrawableRect, radius, radius, mBitmapPaint);
redrawBitmapForSquareCorners(canvas);
}

只是绘制由圆形变成了圆角矩形而已,但是如何只绘制单个原角呢?这里的做法是重绘,例如左上角,重绘的区域即(0,0,20,20)的矩形,将这部分的位图重绘出来就行了。如果是边界,则重绘线段。

这里的问题是发生了重绘,在有三个圆角的情况下最为糟糕,因此应该避免这种情况。如果自己实现,可以将矩形区域划分的细一些,以便一次绘制完毕。也可以采用 Shape 的做法,通过构建路径的方式来实现。

绘制内容的添加

绘制内容的添加即在 ImageView 之上进行扩展绘制。

SlantedTextView库的效果可以采用额外绘制的方法实现。

SwitchIcon库的效果如下

该库所做的额外绘制稍显复杂,包括

  • 绘制斜线状态
  • 达成动画效果
  • 颜色渲染

1.斜线是通过 Paint 绘制 Line 来实现的,其起始点在

1
2
dashXStart = getPaddingLeft() + 0.5f * SIN_45 * dashThickness;
dashYStart = getPaddingTop() + 1.5f * SIN_45 * dashThickness;

结束点在

1
2
dashEnd.x = (int) (dashXStart + width - delta1);
dashEnd.y = (int) (dashYStart + height - delta2);

这样斜线就是从左上向右下逐渐延伸的。

2.斜线的延伸由参数 friction 控制,除此之外,friction 还控制渲染的颜色,透明度的变化。这里原本的 drawable 变色是通过构建新的 PorterDuffColorFilter 来完成的,而斜线的颜色是通过Paint来设置的。

3.这里还有最后一个问题:就是斜线和原 Drawable 的重叠,其解决方法如下

1
2
3
4
5
protected void onDraw(Canvas canvas) {
drawDash(canvas);
canvas.clipPath(clipPath, Region.Op.XOR);
super.onDraw(canvas);
}

这里 clipPath 覆盖斜线,并略微大于斜线,值得注意的是区域截取的方式采用的是 XOR,这保证了Canvas 将在斜线区域之外绘制原 Drawable。

动画效果

AndroidScrollingImageView这种效果实际是视差造成的,动的是背景图,实际绘制的是多个 Bitmap,通过设置 offset 参数造成偏移效果,并通过控制此值形成动画效果。最终效果实际上与背景素材有关,不同的素材可以设置不同的回退速度。

KenBurnsView 库实现 Ken Burns 效果,即景深效果,

首先确定控件的尺寸和Bitmap尺寸是不一致的,后者要大于前者。在 Bitmap 尺寸范围内截取一个空间尺寸大小的区域,同时显示区域用动画移位过去,就是 Ken Burns 效果。

效果的实现与位置形状矩阵 Matrix 有关,这里需要将ScaleType类型设置为MATRIX

1
super.setScaleType(ImageView.ScaleType.MATRIX);

这里移位采用 Matrix 来完成,动画由 mProcess 参数来控制。

图像处理

图像处理主要依赖于颜色矩阵(ColorMatrix)来实现。

ColorMatrix

ColorMatrix 是一个 4*5 的矩阵,4行分别代表red,green,blue和alpha向量,默认是单位阵。在实现上采用的是float数组来存储这些数据。

1
2
3
public class ColorMatrix {    
private final float[] mArray = new float[20];
}

最简单的操作像素颜色变化的方法是 setScale,它只改变了对角线的上的数据,这样颜色的各个分量独立的进行变化

1
2
3
4
5
6
7
8
9
10
public void setScale(float rScale, float gScale, float bScale, float aScale) {
final float[] a = mArray;
for (int i = 19; i > 0; --i) {
a[i] = 0;
}
a[0] = rScale;
a[6] = gScale;
a[12] = bScale;
a[18] = aScale;
}

StyleImageView 库可以进行图像的处理。

图像Bitmap由像素构成,像素又包括对比度(Contrast),亮度(Brightness),纯度(saturation)等参数,图片的重叠还涉及混合模式(Mode)。修改这些信息主要通过颜色矩阵(ColorMatrix)来完成。

关于构建 marix 的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
private static float[] calculateMatrix(int mode, int brightness, float contrast, float saturation) {
return applyBrightnessAndContrast(getMatrixByMode(mode, saturation), brightness, contrast);
}
private static float[] applyBrightnessAndContrast(float[] matrix, int brightness, float contrast) {
float t = (1.0F - contrast) / 2.0F * 255.0F;
for (int i = 0; i < 3; i++) {
for (int j = i * 5; j < i * 5 + 3; j++) {
matrix[j] *= contrast;
}
matrix[5 * i + 4] += t + brightness;
}
return matrix;
}

正常情况下直接改变像素颜色

1
2
final float[] matrix = calculateMatrix(mode, brightness, contrast, saturation);
drawableHolder.getDrawable().setColorFilter(new ColorMatrixColorFilter(new ColorMatrix(matrix)));

如果要在改变时形成动画,则需要利用颜色矩阵中的float数组作为起始值,并利用值动画来更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void updateStyle() {
final float[] matrix = calculateMatrix(mode, brightness, contrast, saturation);
if (enableAnimation) {
animateMatrix(oldMatrix, matrix, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
setDrawableStyleByMatrix(matrix);
}
});
}
}
private void animateMatrix(final float[] startMatrix, final float[] endMatrix, AnimatorListenerAdapter onAnimationEndListener) {
animator = ValueAnimator.ofFloat(0F, 1F).setDuration(animationDuration);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float[] result = new float[20];
float fraction = valueAnimator.getAnimatedFraction();
float progress = interpolator.getInterpolation(fraction);
for (int i = 0; i < 20; i++) {
result[i] = (startMatrix[i] * (1 - progress)) + (endMatrix[i] * progress);
}
drawableHolder.getDrawable().setColorFilter(new ColorMatrixColorFilter(new ColorMatrix(matrix)));
}
});
animator.addListener(onAnimationEndListener);
animator.start();
}